package city.spring.modules.system.service.impl;

import city.spring.modules.system.entity.UserEntity;
import city.spring.modules.system.model.UserDetailsDTO;
import city.spring.modules.system.repository.UserRepository;
import city.spring.modules.system.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.Objects;

/**
 * 用户详细信息服务
 *
 * @author HouKunLin
 * @date 2019/12/3 0003 11:51
 */
@CacheConfig(cacheNames = {UserDetailsServiceImpl.CACHE_NAME})
@Service
public class UserDetailsServiceImpl implements UserDetailsService, UserDetailsPasswordService {
    public final static String CACHE_NAME = "user";
    private final static Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
    private final Cache userCache;
    private final UserService userService;
    private final UserRepository userRepository;

    public UserDetailsServiceImpl(CacheManager cacheManager, UserService userService) {
        this.userCache = Objects.requireNonNull(cacheManager.getCache(UserDetailsServiceImpl.CACHE_NAME));
        this.userService = userService;
        this.userRepository = (UserRepository) userService.getBaseMapper();
    }

    /**
     * 查找用户
     * username被传入用户ID的原因，是在 UserDetails 返回的用户名为用户ID唯一标识符，
     * 以保证同一个用户使用多种登录方式登录的时候能够获取到同一个Token
     * username = 帐号唯一凭据 时：登录过程，第一次获取用户信息
     * username = 用户ID 时：登录过程获取用户信息后可能还会再次获取用户信息，此时传入ID，刷新Token时也是传入用户ID
     *
     * @param loginUseIdentifier 这个有可能是用户ID，也有可能是用户帐号的个人唯一识别号 #UserAccountEntity.identifier
     * @return 返回用户信息
     * @throws UsernameNotFoundException 找不到用户
     */
    @Transactional(rollbackFor = Throwable.class)
    @Override
    @Cacheable(key = "'username:'+#loginUseIdentifier")
    public UserDetails loadUserByUsername(String loginUseIdentifier) throws UsernameNotFoundException {
        logger.debug("使用用户名或者用户ID查找用户信息：{}", loginUseIdentifier);
        UserEntity userEntity;
        try {
            userEntity = userService.getUserByIdOrAccount(loginUseIdentifier, true, UserService.UserInfoEnum.EX_ACCOUNT);
        } catch (Exception e) {
            throw new UsernameNotFoundException(e.getMessage(), e);
        }
        // 同步用户账户信息到用户信息，保证用户账户里面的用户名、手机号、电子邮箱与用户信息里面（冗余信息）的一致
        userService.syncUserAccountToUserInfo(userEntity);

        UserDetailsDTO userDetailsDTO = new UserDetailsDTO(userEntity);
        userDetailsDTO.setLoginUseIdentifier(loginUseIdentifier);

        logger.debug("找到用户信息：{}", userEntity);
        // 把这个用户ID对应的用户信息加入缓存中，因为在接下来的登录过程中，可能会继续使用当前用户的用户ID获取再次用户信息
        // （未完全验证，之前在多个位置配置了UserDetailsService曾导致出现过这个问题），因此直接走缓存即可
        userCache.put(String.format("username:%s", userDetailsDTO.getUsername()), userDetailsDTO);
        return userDetailsDTO;
    }

    /**
     * 系统判定需要修改密码，自动修改数据库中的密码信息。
     * 虽然数据库中的密码信息有改变，但是实际上用户的密码还是跟原来一样的，只是密码编码结果不一样而已
     * 判定条件：PasswordEncoder#upgradeEncoding(java.lang.String)
     *
     * @param user        当前用户，实际上是一个 UserDetailsDTO 对象，就是 public UserDetails loadUserByUsername(String loginUseIdentifier) 获取到的对象
     * @param newPassword 新密码，被重新编码过的新密码（加密状态）
     * @return 返回用户详细信息，可以直接返回传入的user对象
     * @see PasswordEncoder#upgradeEncoding(java.lang.String)
     */
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        logger.debug("系统判断需要修改密码，旧（密）：{}，新（密）：{}", user.getPassword(), newPassword);
        // 此时的 username 实际上是用户ID
        userRepository.updatePassword(user.getUsername(), newPassword);
        if (user instanceof UserDetailsDTO) {
            UserDetailsDTO detailsDTO = (UserDetailsDTO) user;
            detailsDTO.setPassword(newPassword);
        }
        return user;
    }

    @PostConstruct
    public void postConstruct() {
        logger.debug("用户详细信息服务: {}", this);
    }
}
